package ddproto1.debugger.managing;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.log4j.Logger;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.model.Breakpoint;

import com.sun.jdi.event.BreakpointEvent;
import com.sun.jdi.event.Event;
import com.sun.jdi.request.BreakpointRequest;

import ddproto1.commons.DebuggerConstants;
import ddproto1.debugger.eventhandler.IEventManager;
import ddproto1.debugger.eventhandler.IProcessingContext;
import ddproto1.debugger.eventhandler.ProcessingContextManager;
import ddproto1.debugger.eventhandler.processors.IJDIEventProcessor;
import ddproto1.debugger.request.DeferrableBreakpointRequest;
import ddproto1.debugger.request.IDeferrableRequest;
import ddproto1.debugger.request.IResolutionListener;
import ddproto1.util.MessageHandler;
import ddproto1.util.traits.JDIEventProcessorTrait;
import ddproto1.util.traits.JDIEventProcessorTrait.JDIEventProcessorTraitImplementor;

public class JavaBreakpoint extends Breakpoint implements IResolutionListener, JDIEventProcessorTraitImplementor {

    private static final Logger logger = MessageHandler.getInstance().getLogger(JavaBreakpoint.class);
    private static final VMManagerFactory vmmf = VMManagerFactory.getInstance();
    
	private String typeName;
	private int    line;
    
	/** Caches a local map of targets by name. */
    private Map <String, IJavaDebugTarget> targetsByName = 
    	new HashMap<String, IJavaDebugTarget>();
    
    /** Keeps a map of breakpoint requests to targets in order to efficiently
     * dispatch breakpoint events to their corresponding threads.
     */
    private Map<BreakpointRequest, IJavaDebugTarget> targetByRequest = 
    	new HashMap<BreakpointRequest, IJavaDebugTarget>();
    
    /** Keeps a map of deferrable breakpoint request by target. There should
     * be a single request for each target.
     */
    private Map<IJavaDebugTarget, DeferrableBreakpointRequest> deferrableByTarget=
    	new HashMap<IJavaDebugTarget, DeferrableBreakpointRequest>();
    
    private JDIEventProcessorTrait jdiProcessorTrait;
    
    /** State required by the jdi processor trait. */
    boolean enabled;
    IJDIEventProcessor next;
    
    public JavaBreakpoint(){
    	jdiProcessorTrait = new JDIEventProcessorTrait(this);
    }
    	
	public String getModelIdentifier() {
		return DebuggerConstants.PLUGIN_ID;
	}
	
	public void addToTarget(IJavaDebugTarget target)
		throws DebugException
	{
        try{
            VirtualMachineManager vmm = target.getVMManager();
            DeferrableBreakpointRequest dbr = 
                new DeferrableBreakpointRequest(vmm.getName(), typeName, line);
            
            /** We want to hear of all requests generated by this deferrable request. */
            dbr.addResolutionListener(this);
            vmm.getDeferrableRequestQueue().addEagerlyResolve(dbr);

            /** Will be a voter for thread resuming under this target's processing
             * process.
             */
            vmm.getVotingManager().declareVoterFor(IEventManager.RESUME_SET);
            
            synchronized(targetByRequest){
            	setDeferrableRequest(target, dbr);
            	registerTarget(target);
            }
        
        }catch(Exception ex){
            logger.error("Failed to place breakpoint request.", ex);
            cancelForTarget(target);
            return;
        }
	}

    /** Called whenever a deferrable request is fulfilled. 
     * Request won't be enabled before we return. */
    public void notifyResolution(IDeferrableRequest source, Object byproduct) {
        if(!(source instanceof DeferrableBreakpointRequest) ||
        		!(byproduct instanceof BreakpointRequest))
            throw new InternalError("Unexpected request type.");
                
        DeferrableBreakpointRequest dbr = (DeferrableBreakpointRequest)source;
        BreakpointRequest theRequest = (BreakpointRequest)byproduct;

        /** This is to avoid the breakpoint being cancelled for a target
         * that's having a new request added. It could result in garbage
         * being left in the targetByRequest map.
         */
        synchronized(targetByRequest){
        	IJavaDebugTarget target = this.getTarget(dbr.getVMID());
            
            /** Null target means that the breakpoint is in process
             * of being removed for this target. This should be a rare
             * case. 
             * 
             * We can't let this request be enabled, so we re-cancel the
             * deferrable request.
             */
            if(target == null){
                VirtualMachineManager vmm = vmmf.getVMManager(dbr.getVMID());
                try {
                    vmm.getDeferrableRequestQueue().removeRequest(dbr);
                } catch (Exception ex) {
                    logger.error(ex);
                    throw new RuntimeException("Failed to cancel deferrable "
                            + "breakpoint request for target " + target, ex);
                }
            }
   
        	VirtualMachineManager vmm = target.getVMManager();
        	vmm.getEventManager().addEventListener(theRequest, jdiProcessorTrait);
        
        	this.addRequestForTarget(theRequest, target);
        }
    }
    
    /**
     * Specialized process - called whenever one of our event requests is
     * fulfilled (i.e. whenever one of our breakpoints is hit). 
     */
	public void specializedProcess(Event event) {
		
		BreakpointEvent bevt = (BreakpointEvent)event;
		
		VirtualMachineManager _target = vmmf.getVMManager(vmmf
				.getGidByVM(event.request().virtualMachine()));
		String targetName = _target.getName();
		IJavaDebugTarget target = this.getTarget(targetName);
		
		/** Request for cancelled breakpoint. Votes for resuming.*/
		if(target == null){
			IProcessingContext ipc = ProcessingContextManager.getInstance()
					.getProcessingContext();
			ipc.vote(IEventManager.RESUME_SET);
			return; 
		}
		
		/** Gets the thread that was halted by this breakpoint */
		JavaThread thread = target.getVMManager().getThreadManager().getLIThread(
				bevt.thread());
		
		thread.handleBreakpointHit(this);
	}
    
    public void cancelForTarget(IJavaDebugTarget target)
    	throws DebugException
    {
    	synchronized(targetByRequest){
    		/** Removes all requests for this target. */
    		if(!isRegistered(target))
    			this.throwDebugException("Invalid target.");
    		unregisterTarget(target);
    	}
    }
    
    protected void unregisterTarget(IJavaDebugTarget target)
    	throws DebugException
    {
    	/** After this synchronized block completes, this JavaBreakpoint
    	 * instance will no longer recognize the target 'target' as a valid
    	 * target.
    	 */
    	
    	DeferrableBreakpointRequest theRequest = null;
    	
    	synchronized(targetByRequest){
    		Iterator <BreakpointRequest> it = 
    			targetByRequest.keySet().iterator();
    		
    		while(it.hasNext()){
    			IJavaDebugTarget jdtarget = targetByRequest.get(it.next());
    			assert jdtarget != null;
    			if(jdtarget.equals(target)) it.remove();
    		}
    		
    		targetsByName.remove(target.getVMManager().getName());
    		theRequest = deferrableByTarget.remove(target);
    		assert theRequest != null;
    	}
    	
        /** We now attempt to cancel the request associated with this target. 
         * It might have already been cancelled at method notifyResolution()
         * because we released the lock. */
    	try{
    	    target.getVMManager().getDeferrableRequestQueue().removeRequest(theRequest);
    	}catch(Exception ex){
    	    logger.error(ex);
    	    this.throwDebugException("Failed to cancel deferrable " +
    	            "breakpoint request for target " + target, ex);
    	}
    }
    
    protected void registerTarget(IJavaDebugTarget target){
        targetsByName.put(target.getVMManager().getName(), target);
    }
    
    protected IJavaDebugTarget getTarget(String targetName){
        synchronized(targetsByName){
            return targetsByName.get(targetName);
        }
    }
    
    protected boolean isRegistered(IJavaDebugTarget target){
    	return this.targetsByName.containsKey(target.getVMManager().getName());
    }

	public void next(IJDIEventProcessor next) {
		this.next = next;
	}

	public IJDIEventProcessor next() {
		return next;
	}

	public boolean enabled() {
		return enabled;
	}

	public void enabled(boolean newValue) {
		enabled = newValue;
	}
	
	protected void throwDebugException(String exception) 
		throws DebugException {
		this.throwDebugException(exception, null);
	}
	
	protected void throwDebugException(String cause, Throwable t) 
		throws DebugException {
		throw new DebugException(
				new Status(IStatus.ERROR, DebuggerConstants.PLUGIN_ID, 
						IStatus.ERROR, cause, t));
	}
	
	protected void addRequestForTarget(BreakpointRequest theRequest, IJavaDebugTarget target){
		synchronized(targetByRequest){
			targetByRequest.put(theRequest, target);
		}
	}
	
	protected void setDeferrableRequest(IJavaDebugTarget target, DeferrableBreakpointRequest dbr){
		this.deferrableByTarget.put(target, dbr);
	}

}
